/*
  S.M.A.C.K - An operating system kernel
  Copyright (C) 2010,2011 Mattias Holm and Kristian Rietveld
  For licensing and a full list of authors of the kernel, see the files
  COPYING and AUTHORS.
*/

#include <device.h>
#include <panic.h>
#include <stdbool.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <vm.h>
#include <bootparam.h>

#define RAM_DISKS 8
typedef struct {
  uint8_t *data;
  uint8_t *rw_head;

  uint8_t *phy_addr;
  size_t len;

  bool open;
} ramdisk_t;

static ramdisk_t ramdisk[RAM_DISKS];


size_t
ramdisk_read(dev_t *dev, dev_minor_t minor, void *buff, size_t nbytes)
{
  if (minor > RAM_DISKS || !ramdisk[minor].open) return -1;

  if ((uintptr_t)ramdisk[minor].rw_head + nbytes
      < (uintptr_t)ramdisk[minor].data + ramdisk[minor].len) {
    memcpy(buff, ramdisk[minor].rw_head, nbytes);
    ramdisk[minor].rw_head += nbytes;
    return nbytes;
  }

  // TODO: read partial buffer if nbytes is to large
  return -1;
}

size_t
ramdisk_write(dev_t *dev, dev_minor_t minor, const void *buff, size_t nbytes)
{
  if (minor > RAM_DISKS || !ramdisk[minor].open) return -1;

  if ((uintptr_t)ramdisk[minor].rw_head + nbytes
      < (uintptr_t)ramdisk[minor].data + ramdisk[minor].len) {
    memcpy(ramdisk[minor].rw_head, buff, nbytes);
    ramdisk[minor].rw_head += nbytes;
    return nbytes;
  }
  return 0; // TODO: How to signal error when this function returns unsigned?
}

off_t
ramdisk_seek(dev_t *dev, dev_minor_t minor, off_t offset, int whence)
{
  if (minor > RAM_DISKS || !ramdisk[minor].open) return -1;

  switch (whence) {
  case SEEK_SET:
    if (0 <= offset && offset < ramdisk[minor].len) {
      ramdisk[minor].rw_head = ramdisk[minor].data + offset;
      return offset;
    } else {
      return -1;
    }
    break;
  case SEEK_CUR:
    if ((uintptr_t)ramdisk[minor].data <= (uintptr_t)ramdisk[minor].rw_head + offset
        && (uintptr_t)ramdisk[minor].rw_head + offset < (uintptr_t)ramdisk[minor].data + ramdisk[minor].len) {
      ramdisk[minor].rw_head = ramdisk[minor].rw_head + offset;
      return (off_t)ramdisk[minor].rw_head - (off_t)ramdisk[minor].data;
    } else {
      return -1;
    }
    break;
  case SEEK_END:
    if ((uintptr_t)ramdisk[minor].data <= (uintptr_t)ramdisk[minor].rw_head + offset
        && (uintptr_t)ramdisk[minor].rw_head + offset < (uintptr_t)ramdisk[minor].data + ramdisk[minor].len) {
      ramdisk[minor].rw_head = ramdisk[minor].data + ramdisk[minor].len - offset;
      return (off_t)ramdisk[minor].rw_head - (off_t)ramdisk[minor].data;
    } else {
      return -1;
    }
    break;
  default:
    panic();
  }

  printf("ramdisk_seek: code should be unreachable, please ensure that the switch returns");
  panic();
}

int
ramdisk_open(dev_t *dev, dev_minor_t minor)
{
  /* Temporary hack so that we can run fsck on an online filesystem */
  if (minor == 1 && ramdisk[minor].open)
    return 0;

  if (minor > RAM_DISKS || ramdisk[minor].open) return -1;

  // TODO: Ramdisks in kernel region is a bad idea. Fix later
  ramdisk[minor].data = vm_map_physical(vm_get_kernel_map(VM_REG_KERNEL_HEAP),
                                        VM_SUPER_RW | VM_SHARED | VM_DEVICE,
                                        (pa_t)ramdisk[minor].phy_addr,
                                        ramdisk[minor].len);
  ramdisk[minor].rw_head = ramdisk[minor].data;

  if (ramdisk[minor].data) {
    ramdisk[minor].open = true;
    return 0;
  }

  return -1;
}

int
ramdisk_close(dev_t *dev, dev_minor_t minor)
{
  if (minor > RAM_DISKS || !ramdisk[minor].open) return -1;
  vm_unmap(NULL, ramdisk[minor].data);
  ramdisk[minor].open = false;
  return 0;
}

int
ramdisk_ioctl(dev_t *dev, dev_minor_t minor, uint32_t request, uint32_t param)
{
  if (minor > RAM_DISKS) return -1;

  if (request == 0) {
    ramdisk[minor].len = param;
    return 0;
  }

  return -1;
}

static dev_ops_t ramdisk_ops = {
  .read = ramdisk_read,
  .write = ramdisk_write,
  .seek = ramdisk_seek,
  .open = ramdisk_open,
  .close = ramdisk_close,
  .ioctl = ramdisk_ioctl,
};

static dev_t ramdisk_dev = {
  .type = DEV_BLOCK,
  .name = "ramdisk",
  .mountname = "rd",
  .major = DEV_RAMDISK,
  .minor_count = RAM_DISKS,
  .ops = &ramdisk_ops,
};

void
ramdisk_init(void)
{
  int i;
  char dev_name[] = "rd0";

  dev_register(DEV_RAMDISK, &ramdisk_dev);

  for (i = 1; i < RAM_DISKS; i++)
    {
      int len, vlen;
      const char *parameter;
      const char *value;

      /* Considering RAM_DISKS < 10 for now. */
      dev_name[2] = (i - 1) + 0x30;

      parameter = bootparam_get_parameter(dev_name, &len);
      if (!parameter)
        continue;

      value = bootparam_get_value("addr", &vlen,
                                  parameter, len);
      if (value)
        ramdisk[i].phy_addr = strtoptr(value, NULL, 0);

      value = bootparam_get_value("size", &vlen,
                                  parameter, len);
      if (value)
        ramdisk[i].len = strtol(value, NULL, 0);
    }
}
